/*
 * Copyright 2009 Zero Separation
 *
 *     This file is part of PDSUtilities.
 *
 *  PDSUtilities is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  PDSUtilities is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with PDSUtilities.  If not, see <http://www.gnu.org/licenses/>.
 *
 */


package com.zero_separation.pds.utilities.indexingmaps;

import com.sun.sgs.app.AppContext;
import com.sun.sgs.app.ManagedObject;
import com.sun.sgs.app.ManagedObjectRemoval;
import com.sun.sgs.app.ManagedReference;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * ScalableIndexingMap provides a map-like interface to allow data to be stored
 * and accessed through multiple indexes. It does not implement Map as the need
 * to specify indexes makes it incompatible with the Map API.
 * 
 * <p>Internally the data is stored in a list of ScalableHashMap<? V> so most of
 * the constraints and behaviours of ScalableHashMap are followed by this class
 * too. In particular this class should always be accessed using Get rather than
 * GetForUpdate and it will mark itself for update as required.
 *
 * <p>To use this class first create your managed objects in the usual way but
 * all fields which are indexable should be wrapped in an IndexableField.
 *
 * <p>UniqueIndex classes should then be created (these can be anonymous or standard
 * classes) for each IndexableField that wants to be accessed.
 *
 * <p>For example:
<code><pre>
public static class TestIndexableObject implements Serializable {

    IndexableField&lt;Integer&gt; id;
    IndexableField&lt;String&gt; name;

    int getId() {
        return id.get();
    }

    String getName() {
        return name.get();
    }

    void setId(int i) throws KeyAlreadyInIndexException {
        id.set(i);
    }

    void setName(String str) throws KeyAlreadyInIndexException {
        name.set(str);
    }

    public TestIndexableObject(int id, String name) {
        this.id = new IndexableField&lt;Integer&gt;(new Integer(id));
        this.name = new IndexableField&lt;String&gt;(name);
    }

    static class IdIndex extends UniqueIndex&lt;Integer, TestIndexableObject&gt; {

        &#64;Override
        IndexableField&lt;Integer&gt; getKey(TestIndexableObject value) {
            return value.id;
        }

    }

    static class NameIndex extends UniqueIndex&lt;String, TestIndexableObject&gt; {

        &#64;Override
        IndexableField&lt;String&gt; getKey(TestIndexableObject value) {
            return value.name;
        }

    }
}
</pre></code>
 *
 * <p>All changes to the ScalableIndexingMap are made by calling the functions
 * provided in the ScalableIndexingMap itself. The ScalableIndexingMap has no
 * query functions though. All queries are done through the Indexes.
 *
 * @author Tim Boura - Zero Separation
 */
public class ScalableIndexingMap<V> implements Serializable, ManagedObject, ManagedObjectRemoval {

    /** The version of the serialized form of this class. */
    private static final long serialVersionUID = 1L;

    private final List< ManagedReference< Index< V > > > indexes =
            new ArrayList< ManagedReference< Index< V > > >();

    /**
     * Creates a new ScalableIndexingMap with the specified indexes.
     *
     * <p>All ScalableIndexingMaps must have at least one index, any number of
     * additional indexes can then be specified as well. Functionally all indexes
     * are identical and the first one added has no significence apart from being
     * required.
     *
     * @param firstIndex The first index
     * @param otherIndexes Any additional indexes.
     */
    public ScalableIndexingMap( final Index< V > firstIndex, final Index< V >... otherIndexes )
    {
        try {
            addIndex(firstIndex);
            for (Index<V> index : otherIndexes) {
                addIndex(index);
            }
        } catch (IllegalStateException ex) {
            // If adding one index failed we need to delete the already-added ones
            // and then abort.
            for (ManagedReference< Index< V >  > map: indexes) {
                AppContext.getDataManager().removeObject(map);
            }
            throw ex;
        } catch (KeyAlreadyInIndexException ex) {
            Logger.getLogger(ScalableIndexingMap.class.getName()).log(Level.SEVERE, "Duplicate key exception thrown adding empty indexes!", ex);
        }
    }

    /**
     * Adds a new index to the ScalableIndexingMap.
     * 
     * <p>The index will be initialised with the current contents of the map and
     * available for use as soon as this method returns.
     * 
     * @param index The index to add.
     */
    public void addIndex( final Index< V > index ) throws KeyAlreadyInIndexException {
        index.createInternalData();
        if (indexes.size() > 0) {
            for (V value: indexes.get(0).get().getValues())
                index.put(value);
        }
        indexes.add( AppContext.getDataManager().createReference( index ) );
        AppContext.getDataManager().markForUpdate(this);
    }

    /**
     * Adds a new value into the mapping.
     * 
     * <p>The new value is automatically indexed by all of the indexes. Values
     * cannot be added if it will cause a duplicate key in a unique index
     * and if so a KeyAlreadyInIndexException will be thrown. This will never
     * leave the mapping or indexes in an inconsistent state - a duplicate in
     * any unique index prevents entry into all.
     * 
     * @param value The value to add to the mapping.
     * @throws com.zeroseparation.testserver.indexedmaps.mk2.ScalableIndexingMap.KeyAlreadyInIndexException 
     */
    public void put( final V value ) throws KeyAlreadyInIndexException
    {
        for ( ManagedReference< Index< V > > indexRef : indexes )
            indexRef.get().validInsertion(value);

        for ( ManagedReference< Index< V > > indexRef : indexes )
        {
            indexRef.get().put(value);
        }
    }

    /**
     * Removes a value from the mapping and all indexes.
     *
     * @param value The value to remove.
     */
    public void remove( final V value )
    {
        Logger.getLogger(ScalableIndexingMap.class.getName()).log(Level.INFO, "Removing: "+value.toString());
        for ( ManagedReference< Index< V > > mapRef : indexes )
        {
            mapRef.get().remove(value);
        }
    }

    /**
     * Clears the mapping, removing everything it contains but leaving the Indexes
     * ready to recieve more data.
     */
    public void clear() {
        for ( ManagedReference< Index< V > > mapRef : indexes )
        {
            mapRef.get().clear();
        }
    }

    /**
     * Adds every element in the collection into this mapping. If one of the
     * elements would give a duplicate key in any index then an exception will
     * be thrown. In that case the indexes are guaranteed to remain consistent
     * however no guarantees are given about how many of the elements in the
     * Collection may or may not have been added before the exception was
     * generated.
     *
     * @throws com.zeroseparation.testserver.indexedmaps.mk2.ScalableIndexingMap.KeyAlreadyInIndexException
     */
    public void addAll(final Collection<V> c) throws KeyAlreadyInIndexException {
        for (V v: c)
            put(v);
    }

    /**
     * Performs additional operations that are needed when this object is removed.
     *
     * This implementation removes from the DataManager all non-ManagedObject
     * values persisted by this map, as well as objects that make up the internal
     * structure of the map itself.
     */
    public void removingObject() {
        for ( ManagedReference< Index< V > > mapRef : indexes )
        {
            AppContext.getDataManager().removeObject(mapRef.get());
        }
    }


}
